/**
 * File ConcurrentCache.java
 * ---------------------------------------------------------
 *
 * Copyright (C) 2012 David Bauske (david.bauske@googlemail.com)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 *
 * - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 * - The origin of the software must not be misrepresented.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * TL;DR: As long as you clearly give me credit for this Software, you are free to use as you like, even in commercial software, but don't blame me
 * 		if it breaks something.
 */
package net.cl.util.collections;

import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

/**
 * This cache is a thread-safe, high-performance implementation of a value cache for
 * potentially lengthy computations.
 *
 * It scales very well with the number of accessing threads since no cache-wide
 * locks are acquired, but instead locks on a per-argument basis.
 *
 * @param A the argument type to pass to the {@link Computable}. Must implement
 * 		{@link Object#equals(Object)} and {@link Object#hashCode()} properly.
 * @param R the return type of the {@link Computable}.
 *
 * @author David
 */
public class ConcurrentCache<A, R> implements Cache<A, R>
{
	private final ConcurrentMap<A, Future<R>> values = new ConcurrentHashMap<A, Future<R>>();
	private final Computable<A, R> computation;

	/**
	 * Initializes the cache with a computation algorithm. Since a cache caching no
	 * computation results is useless, the computation argument is not optional.
	 *
	 * @param computation The computation to perform. It's compute method might be
	 * 		called by the ConcurrentCache from multiple threads concurrently (but every time with
	 * 		different arguments), so the object must perform appropriate synchronization
	 * 		if needed. (Usually, no synchronization is needed since the Argument object
	 * 		is not shared).
	 * 		May not be null.
	 * @throws NullPointerException if computation is null.
	 */
	public ConcurrentCache(Computable<A, R> computation)
	{
		if(computation == null)
		{
			throw new NullPointerException("computation may not be null.");
		}
		this.computation = computation;
	}

	/**
	 * Gets the computed value for the given argument. If this value wasn't computed yet,
	 * computation is initiated, the result is cached and returned.
	 *
	 * @param argument The argument to get the return value for.
	 * @return The computed value for the given argument.
	 * @throws InterruptedException if the thread is interrupted during computation.
	 */
	@Override
	public R get(final A argument) throws InterruptedException
	{
		Future<R> result = this.values.get(argument);
		if(result == null)
		{
			// Result not cached yet, initiate computation.
			Callable<R> eval = new Callable<R>()
			{
				@Override
				public R call() throws Exception
				{
					return ConcurrentCache.this.computation.compute(argument);
				}
			};

			// FutureTask prevents that two threads compute the result for the same
			// argument concurrently. In this case, the second thread will wait until
			// the first one is done and returns the then-available result.
			FutureTask<R> task = new FutureTask<R>(eval);
			result = this.values.putIfAbsent(argument, task);
			if(result == null)
			{
				result = task;
				task.run();
			}
		}

		try
		{
			return result.get();
		}
		catch(ExecutionException e)
		{
			// Future wraps any exceptions in an ExecutionException, which is
			// inconvenient to deal with.
			Throwable cause = e.getCause();
			if(cause instanceof RuntimeException)
			{
				throw (RuntimeException) cause;
			}
			else if(cause instanceof Error)
			{
				throw (Error) cause;
			}
			else
			{
				// Checked exception. Not allowed.
				throw new IllegalStateException("Illegal checked exception.", cause);
			}
		}
	}

	/**
	 * Deletes a possibly cached value for the given argument.
	 */
	@Override
	public void invalidate(A argument)
	{
		this.values.remove(argument);
	}

	@Override
	public void invalidateAll()
	{
		this.values.clear();
	}

	@Override
	public String toString()
	{
		return "ConcurrentCache [" + this.values.size() + " values]";
	}
}